{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    " 我们分析的目标有\n",
    "- 平均宽度是多少？（一般行动力是多少）\n",
    "    如果成功分析，下一步可以是：\n",
    "    - 逆向ab剪枝，在1h的训练下，能够获得多少棋局的minimax value？ 能够得到多少层的信息？保存到磁盘上可不可行？\n",
    "    - 正向ab剪枝，预测在numba加速的前提下，能够分析多少层？\n",
    "- 出现对称剪枝的情况多不多？\n",
    "\n",
    " - 可以把统计数据记到回合上面，然后最后分析 开局、中局、残局的信息"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "from experiment.评估函数.simulator import Simulator\n",
    "from experiment.评估函数.greedy_ai import GreedyAI\n",
    "import experiment.old_ai.AI as ai\n",
    "from numba import njit\n",
    "\n",
    "import src.project1.submit.AI as new_ai\n",
    "\n",
    "import numpy as np\n",
    "import random\n",
    "\n",
    "# chessboard_size = 4  # 4赛罗游戏\n",
    "\n",
    "chessboard_size = 8  # 正常黑白棋游戏\n",
    "time_out = 5\n",
    "\n",
    "def random_baseline(a, p):\n",
    "    return random.random()  # 0-1随机权重"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "outputs": [],
   "source": [
    "# breadth = [0 for i in range(65)] # 4到64回合可以用\n",
    "breadth = np.zeros(65) # 4到64回合可以用\n",
    "breadth_times = np.zeros(65)\n",
    "def account_breadth(rounds, color, chessboard, agents):\n",
    "    b = len(new_ai.actions(chessboard, color))\n",
    "    breadth[rounds] += b\n",
    "    breadth_times[rounds]+=1"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%%\n"
    }
   }
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "outputs": [],
   "source": [
    "from src.project1.experimental.minimax迭代加深机 import *\n",
    "from numba.typed import Dict\n",
    "from numba import types\n",
    "\n",
    "# @njit\n",
    "def alpha_beta_search_account(chessboard, current_color, a_b_nodes, remaining_depth=8, alphas=np.array([-np.inf, -np.inf])):\n",
    "    \"\"\"\n",
    "\n",
    "    :param chessboard:\n",
    "    :param current_color:\n",
    "    :param remaining_depth: 0 表示 直接对节点估值，不合法。 1表示一层贪心。 根据时间资源和回合数，请合理分配搜索深度。目前知道10层全回合OK的\n",
    "    :param alphas: 0:到目前为止，路径上发现的 color=-1这个agent 的最佳选择值\n",
    "                  1:到目前为止，路径上发现的 color= 1这个agent 的最佳选择值\n",
    "    :param hash_table:\n",
    "    :return: 返回对于chessboard,color这个节点，它最大的选择值是多少，以及它选择了哪个子节点。\n",
    "    \"\"\"\n",
    "    a_b_nodes[0] +=1\n",
    "    alphas = alphas.copy()  # 防止修改上面的alphas\n",
    "    if is_terminal(chessboard):\n",
    "        utility = current_color * get_winner(chessboard)  # winner的颜色和我相等，就是1（颜色的平方性质）， 和我的颜色不等，就是-1.\n",
    "        return min_max_normalized_value(-1, 1, utility), None  # 满足截断性。由于其他价值函数也归一化了，0和1就是最小值和最大值\n",
    "\n",
    "    acts = typed.List(actions(chessboard, current_color))\n",
    "    if len(acts) == 0:\n",
    "        # 只能选择跳过这个action，value为对方的value\n",
    "        value, move = alpha_beta_search_account(chessboard, -current_color, a_b_nodes, remaining_depth - 1, alphas)\n",
    "        return -value, None  # 对手的值是和我反的。 我方没有action可以做。\n",
    "    new_chessboards = typed.List([updated_chessboard(chessboard, current_color, a) for a in acts])  # 用最多10倍内存换一半时间（排序和实际操作共用结果）\n",
    "    hash_table = Dict.empty(\n",
    "        key_type=types.UniTuple(types.uint64, 2),\n",
    "        value_type=types.float64  # 表示节点的价值。 如果哈希表有值，优先使用该值排序\n",
    "    )  # 如果有LRU，可以把继承上一次的table\n",
    "    insertion_sort(acts, new_chessboards, current_color, hash_table, 4)\n",
    "\n",
    "    if remaining_depth <= 1:  # 比如要求搜索1层，就是直接对max节点的所有邻接节点排序返回最大的。\n",
    "        return value_of_positions(new_chessboards[0], current_color), acts[0]  # 评价永远是根据我方的棋盘\n",
    "\n",
    "    value, move = -np.inf, None  # 写在一起。每个节点都尝试让自己的价值最大化\n",
    "    this_color_idx, other_color_idx = int((current_color + 1) // 2), int((-current_color + 1) // 2)\n",
    "    for i, new_chessboard in enumerate(new_chessboards):\n",
    "        action = acts[i]\n",
    "\n",
    "        new_value, t = alpha_beta_search_account(new_chessboard, -current_color, a_b_nodes, remaining_depth - 1, alphas)\n",
    "        new_value = -new_value\n",
    "\n",
    "        if new_value > value:\n",
    "            value, move = new_value, action\n",
    "\n",
    "            alphas[this_color_idx] = max(alphas[this_color_idx], value)\n",
    "        # 另一种颜色的某一个节点已经到达了c = -beta的水平，低于c的都不接受。\n",
    "        # 而我这个节点，至少可以达到v的水平。\n",
    "        # 在那个对手节点看来，我至多会选择-v， 如果它自己的c已经比我这个-v大了，\n",
    "        # 他就不会考虑我，我被剪枝，随便返回一个我的值和选择。\n",
    "        if -value <= alphas[other_color_idx]:\n",
    "            return value, move\n",
    "    return value, move"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%%\n"
    }
   }
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "outputs": [],
   "source": [
    "a_b_nodes = np.zeros(65)\n",
    "a_b_nodes_times = np.zeros(65)\n",
    "\n",
    "def account_a_b_nodes(rounds, color, chessboard, agents):\n",
    "    global a_b_nodes, a_b_nodes_times\n",
    "    a_b_node = np.zeros(1)\n",
    "    alpha_beta_search_account(chessboard, color, a_b_node, 2)\n",
    "    a_b_nodes[rounds] += a_b_node[0]\n",
    "    a_b_nodes_times[rounds]+=1"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%%\n"
    }
   }
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "def do_account_tenth():\n",
    "    # accountants = [account_breadth]\n",
    "    accountants = [account_a_b_nodes]\n",
    "    for i in range(10):\n",
    "        for i_color in [ai.COLOR_BLACK, ai.COLOR_WHITE]:\n",
    "            agents = {i_color: GreedyAI(chessboard_size, i_color, random_baseline),\n",
    "                      -i_color: GreedyAI(chessboard_size, -i_color, random_baseline)}\n",
    "            simulator = Simulator(chessboard_size, time_out, agents)\n",
    "            winner = simulator.quick_run(accountants, True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "spin once!\n",
      "spin once!\n",
      "spin once!\n",
      "spin once!\n",
      "spin once!\n",
      "spin once!\n",
      "spin once!\n",
      "spin once!\n",
      "spin once!\n",
      "spin once!\n",
      "spin once!\n",
      "spin once!\n",
      "spin once!\n",
      "spin once!\n",
      "spin once!\n",
      "spin once!\n",
      "spin once!\n",
      "spin once!\n",
      "spin once!\n",
      "spin once!\n"
     ]
    }
   ],
   "source": [
    "do_account_tenth()"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%%\n"
    }
   }
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "14.55\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "C:\\Users\\YeCanming\\AppData\\Local\\Temp\\ipykernel_2784\\10649458.py:1: RuntimeWarning: invalid value encountered in true_divide\n",
      "  avg_a_b_nodes = np.nan_to_num(a_b_nodes/a_b_nodes_times)\n"
     ]
    },
    {
     "data": {
      "text/plain": "array([ 0.        ,  0.        ,  0.        ,  0.        ,  5.        ,\n        4.        ,  5.45      ,  5.775     ,  6.725     ,  6.85      ,\n        7.35      ,  8.575     ,  8.825     ,  9.325     , 10.25      ,\n       10.775     , 10.35      , 11.55      , 11.15      , 11.975     ,\n       11.275     , 12.325     , 11.5       , 12.7       , 11.925     ,\n       13.8       , 12.75      , 13.625     , 12.675     , 13.8       ,\n       12.525     , 13.75      , 12.825     , 14.325     , 13.075     ,\n       14.55      , 12.85      , 14.2       , 12.375     , 13.75      ,\n       11.975     , 13.35      , 11.5       , 12.525     , 11.1       ,\n       11.6       , 10.8       , 11.375     ,  9.625     , 10.        ,\n        9.2       ,  9.15      ,  8.15      ,  8.25      ,  7.25      ,\n        7.3       ,  5.875     ,  6.125     ,  4.90243902,  4.87804878,\n        3.93023256,  3.6       ,  3.06382979,  2.        ,  0.        ])"
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "avg_a_b_nodes = np.nan_to_num(a_b_nodes/a_b_nodes_times)\n",
    "print(avg_a_b_nodes.max())\n",
    "avg_a_b_nodes"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%%\n"
    }
   }
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[  0.   0.   0.   0. 160. 120. 185. 202. 230. 243. 285. 298. 322. 325.\n",
      " 359. 357. 364. 399. 389. 414. 440. 423. 461. 451. 456. 486. 461. 474.\n",
      " 491. 479. 497. 469. 516. 490. 519. 495. 510. 469. 487. 463. 483. 445.\n",
      " 464. 434. 430. 405. 420. 362. 380. 340. 339. 306. 300. 267. 278. 222.\n",
      " 212. 183. 166. 139. 113.  89.  66.  40.   0.]\n",
      "[ 0.  0.  0.  0. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40.\n",
      " 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40.\n",
      " 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40.\n",
      " 40. 40. 40. 41. 40. 40. 40. 42. 46. 57.  0.]\n"
     ]
    }
   ],
   "source": [
    "print(breadth)\n",
    "print(breadth_times)    # 第六十四回合永远terminal，所以nan"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%%\n"
    }
   }
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ 0.          0.          0.          0.          4.          3.\n",
      "  4.625       5.05        5.75        6.075       7.125       7.45\n",
      "  8.05        8.125       8.975       8.925       9.1         9.975\n",
      "  9.725      10.35       11.         10.575      11.525      11.275\n",
      " 11.4        12.15       11.525      11.85       12.275      11.975\n",
      " 12.425      11.725      12.9        12.25       12.975      12.375\n",
      " 12.75       11.725      12.175      11.575      12.075      11.125\n",
      " 11.6        10.85       10.75       10.125      10.5         9.05\n",
      "  9.5         8.5         8.475       7.65        7.5         6.675\n",
      "  6.95        5.55        5.3         4.46341463  4.15        3.475\n",
      "  2.825       2.11904762  1.43478261  0.70175439  0.        ]\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "C:\\Users\\YeCanming\\AppData\\Local\\Temp\\ipykernel_14172\\2469600728.py:1: RuntimeWarning: invalid value encountered in true_divide\n",
      "  avg_breadths = np.nan_to_num(breadth/breadth_times)\n"
     ]
    }
   ],
   "source": [
    "avg_breadths = np.nan_to_num(breadth/breadth_times)\n",
    "print(avg_breadths)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "outputs": [
    {
     "data": {
      "text/plain": "8.093369219197761"
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "avg_breadths.mean()"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%%\n"
    }
   }
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "outputs": [
    {
     "data": {
      "text/plain": "<Figure size 432x288 with 1 Axes>",
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYsAAAEGCAYAAACUzrmNAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8qNh9FAAAACXBIWXMAAAsTAAALEwEAmpwYAAApv0lEQVR4nO3deXwURfrH8U+BHIoIGDWywAqsiBJAjoAgIBEU8FjAY1dcFRQVRcRrXQX9eeCtCygoHigsoFyCJygiV0Tkjtx3RJSwHModNECS+v1RDRshISHMTM90vu/Xa17pqenpeSqMeXyququNtRYREZFjKeZ3ACIiEv2ULEREJF9KFiIiki8lCxERyZeShYiI5OskvwMIhzPOOMNWrVq1UO/dt28fZcqUCW1AUSKofQtqvyC4fQtqvyC2+5aSkvKrtfbM3F4LZLKoWrUqCxcuLNR7k5OTSUpKCm1AUSKofQtqvyC4fQtqvyC2+2aM+Smv1zQMJSIi+VKyEBGRfClZiIhIvgI5Z5GbgwcPkpaWRkZGxjH3K1euHKtWrYpQVJEVrr6VLl2aypUrU6JEiZAfW0SiQ5FJFmlpaZQtW5aqVatijMlzv71791K2bNkIRhY54eibtZbt27eTlpZGtWrVQnpsEYkeRWYYKiMjg7i4uGMmCjl+xhji4uLyrdhEJLYVmWQBKFGEiX6vItFhzhx48UX3M9SKVLIQEQmqObMt77d8j3n/N4HWrUOfMJQsYtitt97K+PHjQ3rMU089NaTHE5EISE2l4i2tefPgndyQPYoDByA5ObQfoWRRRD399NMMGzbM7zBE5ERkZkLfvlC3LpW3ptCjxGA6FxtJyZIQ6ovIlSyOIZTjf08++SSvvfba4eePP/44AwYMOOHjzpw5k4svvpjq1asfrjI++eQTWrdujbWWzZs3c95557Fly5YCH/PBBx8kISGB1q1b88svv5xwjCISBkuXQtOm8K9/weWXc9Kaldz8zZ0881wxpk1zL4WSkkUe5syB1q3hiScIyfhf165dGTFiBADZ2dmMGTOGm2+++aj9WrRoQb169Y56TJ06Ndfjbt68mVmzZjFx4kR69eoFwDXXXEPFihUZNGgQd955J3369OHss88uUJz79u0jMTGRFStW0LJlS/r06VPIHotIWOzf7/4wNWwIP/8MY8fCp59CpUo0bQq9e4c+UUARus7ieCUnw4EDkJXF4fG/E/kHqFq1KnFxcSxatIitW7dSv3594uLijtrv22+/Pa7jduzYkWLFilGrVi22bt16uP3111+ndu3aNGnShBtvvBGAFStWcPfddwOwZcsWSpYsebjamTZtGnFxcRQrVowbbrgBgJtvvplrr722MN0VkXD47ju44w5YvRo6d4b+/SGXvyPhoGSRh6QkKFnSJYpQjf/dcccdDBs2jC1bttC1a9dc92nRogV79+49qr1v375cdtllR7WXKlXq8La19vB2WloaxYoVY+vWrWRnZ1OsWDESEhJYvHgx4OYsqlatyq233nrMmHVarEgU2LsXHnsMBg2CKlXgq6+gbduIhqBkkYemTWHaNFdRJCWFpqy75pprePLJJzl48CCjRo3KdZ/jrSxyk5mZSdeuXRk9ejTDhw+nf//+PPzwwwV6b3Z2NuPHj6dTp06MGjWK5s2bn3A8InICJk2Cu+6CtDTo2ROefx58OGtRyeIYmjYN7dhfyZIlufTSSylfvjzFixcP3YGP8MILL9CiRQuaN2/OhRdeSKNGjbjqqquoXLlyvu8tU6YM8+fP57nnnuOss85i7NixYYtTRI7h11/hwQfhgw/gggvcEFQ4JiMKSMkigrKzs5k7dy7jxo0LyfGOPPU1PT0dcGdeHVK2bFlWr14N8IfhraeffjrXYx46hoj4xFo3aX3ffbBzJzz5pBuCyjHk7AedDRUhK1eu5Nxzz6V169bUqFHD73BEJBqlpUH79nDjjVC1Knz/PfTp43uigAhUFsaY4sBCYJO19mpjTDVgDBAHpAC3WGsPGGNKASOAhsB24AZr7QbvGL2B24Es4D5r7eRwxx1qtWrVYv369X6HISLRKDsbBg+GRx5xF9r16wf33w9hHK4+XpGoLO4Hct5E4WXgVWvtucBOXBLA+7nTa3/V2w9jTC2gE5AAtAPe9BKQiEjsW7sWLr0UuneHRo1g+XJ46KGoShQQ5mRhjKkMXAW85z03QCvg0IJGw4GO3nYH7zne6629/TsAY6y1+621PwKpQONwxi0iEnYHD8JLL0HdurBkCQwZAlOnQvXqfkeWq3APQ70GPAIcuuNOHLDLWpvpPU8DKnnblYCNANbaTGPMbm//SsDcHMfM+R4RkdizaBHcfrv7ee218MYbULGi31EdU9iShTHmamCbtTbFGJMUrs/J8XndgG4A8fHxJB+x5GK5cuVyvdjtSFlZWQXaLxaFs28ZGRlH/c4jJT093bfPDreg9i2o/YJj963Y/v2cM2IEfx4zhgPly7OuTx9+veQSWLPGPaKZtTYsD+BFXBWwAdgC/AaMBH4FTvL2aQpM9rYnA0297ZO8/QzQG+id47iH98vr0bBhQ3uklStXHtWWmz179hRov1h0ZN82bdpkr7vuupAcu6C/33CYMWOGb58dbkHtW1D7Ze0x+vbNN9bWqGEtWNu1q7U7dkQ0roIAFto8/q6Gbc7CWtvbWlvZWlsVN0E93Vp7EzADuN7brQvwmbf9ufcc7/XpXvCfA52MMaW8M6lqAPPDFXekWGvJzs4u9PszMzPz3ykff/rTn0J+PwwROcKePW7yumVLd6bTlClufqJCBb8jOy5+XGfxKPCQMSYVNycxxGsfAsR57Q8BvQCstSuAD4GVwFdAD2ttVsSjDoENGzZQs2ZNOnfuTO3atXn22Wdp1KgRdevW5amnnjq837PPPkvNmjVp3rw5N954I3379gUgKSmJBx54gMTERAYMGEBKSgotW7akYcOGtG3bls2bNwMwcOBAatWqRd26denUqRMA33zzDc2aNaNevXrUr1+fvXv3smHDBmrXrg24YaTbbruNOnXqUL9+fWbMmAG4C/+uvfZa2rVrR40aNXjkkUci+SsTiW0TJ0JCgjst9qGHYNkyyGWNt1gQkSu4rbXJQLK3vZ5czmay1mYAf8vj/c8Dz4csoAceAG9BvSOdnJVVuFPW6tWDHPeryMu6desYPnw4e/bsYfz48cyfPx9rLe3bt2fmzJmcfPLJfPTRRyxZsoSDBw/SoEEDGjZsePj9Bw4cYOHChRw8eJCWLVvy2WefceaZZzJ27Fgef/xxhg4dyksvvcSPP/5IqVKl2LVrF+AWIuzXrx+XX3456enplC5d+g9xDRo0CGMMy5YtY/Xq1bRp04a1a9cCsHjxYhYtWkSpUqWoWbMmPXv2pEqVKsf/OxIpKrZtc9dJjBkDtWvDRx9B49g+iVPLfUTYOeecQ5MmTXj44Yf5+uuvqV+/PuAmxdatW8fevXvp0KEDpUuXpnTp0vz1r3/9w/sPLR++Zs0ali9fzuWXXw64yeuK3tkUdevW5aabbqJjx4507NgRgGbNmtG7d29WrVrFtddee9Q6UbNmzaJnz54AnH/++ZxzzjmHk0Xr1q0pV64c4C4u/Omnn5QsRHJjLfFTpsD117vhpz59oFcvt3R1jCuayeIYFcDve/dStmzZPF8/UWXKlAHcnEXv3r256667jggt79iOfH9CQgJzcrkr0xdffMHMmTOZMGECzz//PMuWLaNXr14kJSUdHo6aPHnyUdVFXnIug168ePGQzJeIBM7PP8Pdd3PBpEnQpAm8954bggoIrQ3lk7Zt2zJ06NDDC/dt2rSJbdu20axZMyZMmEBGRgbp6elMnDgx1/fXrFmTX3755XCyOHjwICtWrCA7O5uNGzdy6aWX8vLLL7N7927S09P54YcfSEhI4NFHH6VRo0aHFxc8pEWLFowcORKAtWvX8vPPP1OzZs0w/gZEAiI7291nIiEBZs5k3b33wqxZgUoUUFQriyjQpk0bVq1aRVNvyeFTTz2VDz74gEaNGtG+fXvq1q1LfHw8derUOTwElFPJkiUZP3489913H7t37yYzM5MHHniA8847j5tvvpndu3djreW+++6jfPnyPPHEE0ybNo2TTjqJhIQErrjiisMT4gD33HMP3bt3p06dOpx00kkMGzbsDxWFiORi9Wp357rvvoM2beCdd9i0YQM1omypjpDI65zaWH7E+nUWe/futdZau2/fPtuwYUObkpISkuOGs2+6ziI8gtq3mO/XgQPWPvectSVLWluhgrXDh1ubnW2tje2+cYzrLFRZRKFu3bqxcuVKMjIy6NKlCw0aNPA7JBE5ZOFCt1TH0qXw97/DwIEQH+93VGGnZBGF8rrlqoj46Lff4KmnoH9/OPts+PRT6NDB76gipkglC2stbiFbCSVXvYoE2IwZcOed8MMP0K0bvPwylC/vd1QRVWTOhipdujTbt2/XH7YQs9ayffv2Ap+GKxJTdu1ySaJVKzDGJY133ilyiQKKUGVRuXJl0tLS+OWXX465X0ZGRmD/8IWrb6VLlz7qIj+RmPfpp3DPPbB1q7uD3dNPw8kn+x2Vb4pMsihRogTVqlXLd7/k5OTDV1UHTZD7JhIyW7dCz54wbhxceCFMmAA5ltwpqorMMJSIyDFZC8OHwwUXwOefw/PPw4IFShSeIlNZiIjkacMGuOsu+PpraNbMLdVx/vl+RxVVVFmISNGVlQUDBrilOWbPdst2zJypRJELVRYiUjStWOEurps3D668Et56C/78Z7+jilqqLESkaDlwwC0dXr8+pKbCyJHuJkVKFMekykJEio5589zCf8uXwz/+4W5XcOaZfkcVE1RZiEjw7dvnbmvatKm70G7iRFdRKFEUmCoLEQm2qVPdEh0//gjdu8NLL8Fpp/kdVcxRZSEiwbRzJ3TtCpdfDiVKwDffwJtvKlEUkpKFiATPRx+5i+tGjIDevWHJErjkEr+jimkahhKR4Ni8GXr0gE8+gQYN4KuvoF49v6MKBFUWIhL7rIUhQ1w1MWmSW0J83jwlihBSZSEise3QPSamT4eWLeHdd6FGDb+jChxVFiISm7KyoF8/qFPH3er07bddwlCiCAtVFiISe5YudRfXLVgAf/2rO8tJ91QJK1UWIhI79u+HJ55wy4Zv2ABjxsBnnylRRIAqCxGJDbNnu2pi1Sq45RZ49VWIi/M7qiJDlYWIRLf0dLjvPmje3C3bMWmSu35CiSKilCxEJHp99ZW718Qbb8C997oFANu18zuqIknJQkSiz/bt0LkzXHEFnHIKzJoFAwdC2bJ+R1ZkKVmISPSwFsaOdRfXjR7tJrMXL4aLL/Y7siJPE9wiEh3S0uCee2DCBEhMdKvF1q3rd1TiUWUhIv7KzoZ33nFzE1OnQt++MGeOEkWUUWUhIv5Ztw7uvNMtH96qFQweDH/5i99RSS5UWYhIxJmsLHjlFVc9LF4M773nqgoliqilykJEImvxYhp07+6qimuucafF/ulPfkcl+VBlISKRkZEBjz0GiYmU+vVXGD8ePv5YiSJGhC1ZGGNKG2PmG2OWGGNWGGP6eO3VjDHzjDGpxpixxpiSXnsp73mq93rVHMfq7bWvMca0DVfMIhIm334LF14IL74InTszf9gwuO46v6OS4xDOymI/0MpaeyFQD2hnjGkCvAy8aq09F9gJ3O7tfzuw02t/1dsPY0wtoBOQALQD3jTGFA9j3CISKnv2uNNhL7kEDhyAr7+GoUPJ1H2wY07YkoV10r2nJbyHBVoB47324UBHb7uD9xzv9dbGGOO1j7HW7rfW/gikAo3DFbeIhMgXX7jTYd9+Gx580C3VcfnlfkclhRTWCW6vAkgBzgUGAT8Au6y1md4uaUAlb7sSsBHAWptpjNkNxHntc3McNud7cn5WN6AbQHx8PMnJyYWKOT09vdDvjXZB7VtQ+wWx2bcSu3Zx7uuvEz99OvuqVmX1G2+wt1Ytd+8JTyz2q6CC2rewJgtrbRZQzxhTHvgEOD+MnzUYGAyQmJhok5KSCnWc5ORkCvveaBfUvgW1XxBjfbMWRo2C++93w099+lCmVy8alix51K4x1a/jFNS+ReRsKGvtLmAG0BQob4w5lKQqA5u87U1AFQDv9XLA9pztubxHRKLBxo1w9dVw883utqaLFsGTT0IuiUJiUzjPhjrTqygwxpwMXA6swiWN673dugCfedufe8/xXp9urbVeeyfvbKlqQA1gfrjiFpHjkJ0NgwZBrVqQnAyvveZWiE1I8DsyCbFwDkNVBIZ78xbFgA+ttRONMSuBMcaY54BFwBBv/yHA+8aYVGAH7gworLUrjDEfAiuBTKCHN7wlIn5avdot1TFrlpu4fucdqFbN76gkTMKWLKy1S4H6ubSvJ5ezmay1GcDf8jjW88DzoY5RRArh4EH497+hTx8oUwaGDXP3njDG78gkjLTch4gUXEoK3H47LFkCf/sbvP46xMf7HZVEgJb7EJH8/fYbPPIING4M27bBJ5/Ahx8qURQhqixE5NiSk93cRGqq+/nKK1C+vN9RSYSpshCR3O3aBd26waWXumsopk9395tQoiiSlCxE5GiffeZOfx0yBB5+GJYudUlDiiwlCxH5n61b4YYboGNHOOMMmDfPnfl0yil+RyY+U7IQETfMNGIEXHABfPopPPccLFwIiYl+RyZRQhPcIkXdhg1w111u+fBmzdwtTs8P2zJuEqNUWYgUVVlZMHAg1K4Ns2e725vOnKlEIblSZSFSFK1c6S6umzsXrrjC3XPiz3/2OyqJYqosRIqSAwfgmWegfn1Ytw7ef9/dpEiJQvKhykKkqJg/31UTy5dDp04wYACcdZbfUUmMUGUhEnT79sFDD0HTprBzJ0yYAKNHK1HIcVFlIRJk06a5JTp+/BG6d4eXXoLTTvM7KolBqixEgmjnTjfkdNllcNJJ8M038OabShRSaEoWIkHz8cfuznXDh0OvXm458Usu8TsqiXEahhIJis2b4d57XbKoXx++/NL9FAkBVRYisc5aGDrUVRNffOHmJebNU6KQkFJlIRLL1q93y4hPm+aGmt59F847z++oJIBUWYjEoqws6N/fLdUxfz689RbMmKFEIWGjykIk1ixbBnfc4ZLE1Ve7RFG5st9RScCpshCJFfv3w1NPQYMG7rqJ0aPh88+VKCQiVFmIxII5c9x1E6tWwc03w6uvupsTiUSIKguRKFb899/h/vvdfSbS093psO+/r0QhEafKQiRaTZ5Mo9tug23boEcPeOEFKFvW76ikiFJlIRJttm+HLl2gXTuySpWCb7+F119XohBfqbIQiRbWwrhx0LMn7NgB//d/pLRowSXNmvkdmYgqC5GosGkTdOwIN9zgbkSUkgLPPkt2yZJ+RyYCKFmI+Cs7GwYPdkt1TJkCffu6M5/q1vU7MpE/0DCUiF9SU929JpKT4dJL3VIdf/mL31GJ5EqVhUikZWbCK69AnTqwaJFLEtOmKVFIVFNlIRJJixe7i+u+/97NUQwaBH/6k99RieQr38rCGNPTGFMhEsGIBFZGBjz+OCQmQlqaO+vp44+VKCRmFGQYKh5YYIz50BjTzhhjwh2USKDMmgX16rmL6m65xS3Zcf31oP+UJIbkmyystf8H1ACGALcC64wxLxhjNMAqcix79rgrr1u0cIsATp4M//kPnH6635GJHLcCTXBbay2wxXtkAhWA8caYV8IYm0js+uILSEhwy4c/8IBbVrxNG7+jEim0gsxZ3G+MSQFeAb4D6lhruwMNgevCHJ9IbPnlF7jpJnefidNOg9mz3Qqxp57qd2QiJ6QglcXpwLXW2rbW2nHW2oMA1tps4Oq83mSMqWKMmWGMWWmMWWGMud9rP90YM8UYs877WcFrN8aYgcaYVGPMUmNMgxzH6uLtv84Y0+WEeiwSDtbCqFHu4rpx49x9J77/Hpo08TsykZAoyJzFU9ban/J4bdUx3poJ/NNaWwtoAvQwxtQCegHTrLU1gGnec4ArcHMjNYBuwFvgkgvwFHAR0Bh4SmdnSVTZuBH++ldXUfzlLy5JPP00lCrld2QiIRO2i/KstZuttd9723uBVUAloAMw3NttONDR2+4AjLDOXKC8MaYi0BaYYq3dYa3dCUwB2oUrbpECy852cxIJCe7+16++Ct995+6LLRIwEbkozxhTFagPzAPirbWbvZe24E7NBZdINuZ4W5rXllf7kZ/RDVeREB8fT3JycqFiTU9PL/R7o11Q++ZHv07++Wdq9utH+aVL2dGwIWv/+U8yKlZ0y4mHkP7NYk9Q+xb2ZGGMORX4CHjAWrsn52Ua1lprjLGh+Bxr7WBgMEBiYqJNSkoq1HGSk5Mp7HujXVD7FtF+HTzoFvvr0wdOPhn+8x9O79KFJmG6ZkL/ZrEnqH0L69pQxpgSuEQx0lr7sde81Rtewvu5zWvfBFTJ8fbKXlte7SKRlZICjRvDY4+5OYpVq+DWW3VxnRQJYUsW3pXeQ4BV1tr+OV76HDh0RlMX4LMc7Z29s6KaALu94arJQBtjTAVvYruN1yYSGb//Do8+ChddBFu2uGU6xo2Ds8/2OzKRiAnnMFQz4BZgmTFmsdf2GPAS8KEx5nbgJ+Dv3mtfAlcCqcBvwG0A1todxphngQXefs9Ya3eEMW6R/0lOdsuIp6a6BQD//W+ooJPxpOgJW7Kw1s4C8qrPW+eyvwV65HGsocDQ0EUnko/du+GRR9yNiapXd0uIt2rld1QivtH9LESO9Pnn7uK6996Dhx92S3UoUUgRp2Qhcsi2bdCpE3ToAHFxMHeuG3Y65RS/IxPxnZKFiLUwYgRccAF88gk8+ywsXAiNGvkdmUjU0J3ypGj76Se46y63fPjFF7uhpwsu8DsqkaijykKKpqwseP11t1THrFlu+9tvlShE8qDKQoqelSvhjjtgzhxo1w7efhvOOcfvqESimioLKToOHHDzEfXrw5o18P778OWXShQiBaDKQoqGBQvcRXXLlrkzngYMgLPO8jsqkZihykKC7bff3LUSTZrAjh3uGorRo5UoRI6TKgsJrmnToFs3WL/enfH08stQrpzfUYnEJFUWEjw7d7ohp8sug2LF3PpOb7+tRCFyApQsJFg+/tgt1TF8uFspdulSaNnS76hEYp6GoSQYtmyBe++Fjz6CevXgiy+gQQO/oxIJDCULiW3WcvakSXDNNe6+Ey++CP/8J5Qo4XdkIoGiZCGxy5u4Pn/qVGjRAt59F2rW9DsqkUDSnIXEnqwsePVVqFMH5s1j7YMPuklsJQqRsFGykNiyfLlb8O+hh+DSS2HFCv7bvr0760lEwkb/hUls2L8fnnrKTVqvXw+jRsGECVClit+RiRQJmrOQ6Dd3rrtuYuVKuOkmeO01OOMMv6MSKVJUWUj0Sk+HBx5ww05797pF/z74QIlCxAeqLCQ6ff21W6rjp5+gRw93SmzZsn5HJVJkqbKQ6LJjB9x6K7RtC6VLuxsSvfGGEoWIz5QsJDpYC+PGuTvVjRwJjz8OixdD8+Z+RyYiaBhKosF//wv33AOffQYNG7ohqAsv9DsqEclBlYX4x1p31XWtWjB5MrzyijvzSYlCJOqoshB/pKa6CewZMyApySWNc8/1OyoRyYMqC4mszEzo29ct1ZGSAoMHw/TpShQiUU6VhUTOkiXu4rqUFOjQAQYNgkqV/I5KRApAlYWEX0aGO7spMRE2boQPP4RPPlGiEIkhqiwkvGbNgjvugDVroEsX6NcP4uL8jkpEjpMqCwmPvXvdnetatHCVxVdfwbBhShQiMUrJQkLvyy8hIQHefBPuv98tK962rd9RicgJ0DCUhM6vv7qF/0aOdNdOfPcdNG3qd1QiEgKqLOTEWQujR7ulOj780N134vvvlShEAkSVhZyYtDTo3h0mToTGjWHIEKhd2++oRCTEVFlI4WRnw1tvueGm6dOhf3+YPVuJQiSgVFnI8Vu71p0O++230Lq1uwq7enW/oxKRMApbZWGMGWqM2WaMWZ6j7XRjzBRjzDrvZwWv3RhjBhpjUo0xS40xDXK8p4u3/zpjTJdwxSsFcPAgvPQS1K0Ly5bB0KEwZYoShUgREM5hqGFAuyPaegHTrLU1gGnec4ArgBreoxvwFrjkAjwFXAQ0Bp46lGAkwr7/3s1J9O4NV18Nq1bBbbeBMX5HJiIRELZkYa2dCew4orkDMNzbHg50zNE+wjpzgfLGmIpAW2CKtXaHtXYnMIWjE5CE0++/Q69eLlFs2QIffQTjx8PZZ/sdmYhEUKTnLOKttZu97S1AvLddCdiYY780ry2v9qMYY7rhqhLi4+NJTk4uVIDp6emFfm+0O96+lVuyhJp9+3JKWhqbr7ySH+6+m8yyZSHKfj/6N4s9Qe0XBLdvvk1wW2utMcaG8HiDgcEAiYmJNikpqVDHSU5OprDvjXYF7tvu3fDoo/DOO24+YupUKrZuTcWwR1g4+jeLPUHtFwS3b5E+dXarN7yE93Ob174JqJJjv8peW17tEi4TJrilOt59Fx56CJYudWc8iUiRFulk8Tlw6IymLsBnOdo7e2dFNQF2e8NVk4E2xpgK3sR2G69NQm3bNujUCdq3hwoVYM4ct0JsmTJ+RyYiUSBsw1DGmNFAEnCGMSYNd1bTS8CHxpjbgZ+Av3u7fwlcCaQCvwG3AVhrdxhjngUWePs9Y609ctJcToS18MEHbk2n9HR45hk3BFWypN+RiUgUCVuysNbemMdLR41pWGst0COP4wwFhoYwNDnkp5/g7rvd8uFNm8J777krskVEjqDlPoqi7Gx44w03N/HttzBwoPupRCEiedByH0XNqlVuqY7Zs909Jt55B845x++oRCTKqbIoKg4c4Jz334d69WD1ahgxAiZNUqIQkQJRZVEULFgAt99OtWXL4IYbYMAAiI/P/30iIh5VFkH222/w8MPQpAls386y556DMWOUKETkuClZBNX06VCnjrtW4s47YeVKtjdr5ndUIhKjlCyCZtculxxat4Zixdw6Tm+/DeXK+R2ZiMQwJYsg+fRTd/rrf/4Djzzilupo2dLvqEQkADTBHQRbtkDPnm7p8AsvdOs7NWzod1QiEiCqLGKZtTBsmKsmJkyAF15wZz4pUYhIiKmyiFU//gh33eVua9q8uVuqo2ZNv6MSkYBSZRFrsrLgtdegdm23MuygQfDNN0oUIhJWqixiyYoVcPvtMG8eXHUVvPUWVKmS//tERE6QKotYcOAA9OkD9evDDz/AyJFujkKJQkQiRJVFtJs71y38t2IF/OMfbgjqzDP9jkpEihhVFtEqPd3dkOjii909sSdOdBWFEoWI+ECVRTSaMgW6dYMNG+Cee+DFF+G00/yOSkSKMFUW0WTHDrjtNmjTxt3WdOZMd7aTEoWI+EzJIhpY666+rlUL3n8fHnsMliyBFi38jkxEBNAwlP/++1/o0cOt69Sggbsfdr16fkclIvIHqiz8Yq276rpWLZcgXnnFXT+hRCEiUUiVhR9SU90E9owZblXYd9+FGjX8jkpEJE+qLCIpMxP69oW6dSElBd55x92kSIlCRKKcKotIWbrULdWxcCG0bw9vvgmVKvkdlYhIgaiyCLf9++GJJ9yy4T//DGPHuslsJQoRiSGqLMLpu+/cUh2rV0PnztC/P8TF+R2ViMhxU2URDnv3ujvXtWgBv/3mznYaPlyJQkRilpJFqE2a5O41MWiQSxgrVkDbtn5HJSJyQpQsQuXXX+GWW+DKK6FMGTcENWAAnHqq35GJiJwwJYsTZS2MGeMurhszxk1mL1oETZv6HZmISMhogvtEpKVB9+5u+fBGjWDaNKhTx++oRERCTpVFYWRnw9tvu2pi2jTo18/dD1uJQkQCSpXF8Vq7Fu680y0f3qqVW6qjenW/oxIRCStVFgWVmQkvv+yW6liyBIYMgalTlShEpEhQZVEQixa5pToWLYJrr4U33oCKFf2OSkQkYpQscjFnDiQnQ6umv3PR5Gfg3/+GM85wNyi67jq/wxMRiTgliyOsWHEa//oXXLR/JtfbO8Cug65d3WqxFSr4HZ6IiC9iZs7CGNPOGLPGGJNqjOkVrs9ZPb84/TO6MyO7JcVtJqO7TnHzE0oUIlKExUSyMMYUBwYBVwC1gBuNMbVC/kEpKTz7cTvutIN5lQdpXHoZVe+4LOQfIyISa2JlGKoxkGqtXQ9gjBkDdABWhvJDFvxSlfR9F/CY+ZSFxS9i0ABdiC0iArGTLCoBG3M8TwMuyrmDMaYb0A0gPj6e5OTk4/6QkR/9maFmKtnZxSiWnc2CBRs477yfCx91lElPTy/U7yXaBbVfENy+BbVfENy+xUqyyJe1djAwGCAxMdEmJSUd9zFKlYL3388iMxNKlixG167Vado0ONdRJCcnU5jfS7QLar8guH0Lar8guH2LlWSxCaiS43llry2kmjaFfv2WsGdPA5KSNAQlInJIrCSLBUANY0w1XJLoBPwjHB+UkLCHAP5PgYjICYmJZGGtzTTG3AtMBooDQ621K3wOS0SkyIiJZAFgrf0S+NLvOEREiqKYuM5CRET8pWQhIiL5UrIQEZF8KVmIiEi+jLXW7xhCzhjzC/BTId9+BvBrCMOJJkHtW1D7BcHtW1D7BbHdt3OstWfm9kIgk8WJMMYstNYm+h1HOAS1b0HtFwS3b0HtFwS3bxqGEhGRfClZiIhIvpQsjjbY7wDCKKh9C2q/ILh9C2q/IKB905yFiIjkS5WFiIjkS8lCRETypWSRgzGmnTFmjTEm1RjTy+94cmOMGWqM2WaMWZ6j7XRjzBRjzDrvZwWv3RhjBnr9WWqMaZDjPV28/dcZY7rkaG9ojFnmvWegMcZEqF9VjDEzjDErjTErjDH3B6hvpY0x840xS7y+9fHaqxlj5nnxjDXGlPTaS3nPU73Xq+Y4Vm+vfY0xpm2Odt++u8aY4saYRcaYiQHr1wbv+7LYGLPQa4v572OhWWv1cPM2xYEfgOpASWAJUMvvuHKJ8xKgAbA8R9srQC9vuxfwsrd9JTAJMEATYJ7Xfjqw3vtZwduu4L0239vXeO+9IkL9qgg08LbLAmuBWgHpmwFO9bZLAPO8OD4EOnntbwPdve17gLe97U7AWG+7lve9LAVU876vxf3+7gIPAaOAid7zoPRrA3DGEW0x/30s7EOVxf80BlKtteuttQeAMUAHn2M6irV2JrDjiOYOwHBvezjQMUf7COvMBcobYyoCbYEp1tod1tqdwBSgnffaadbaudZ9m0fkOFZYWWs3W2u/97b3Aqtw914PQt+stTbde1rCe1igFTDeaz+yb4f6PB5o7f1fZwdgjLV2v7X2RyAV97317btrjKkMXAW85z03BKBfxxDz38fCUrL4n0rAxhzP07y2WBBvrd3sbW8B4r3tvPp0rPa0XNojyhueqI/7P/BA9M0bqlkMbMP9wfgB2GWtzcwlnsN98F7fDcRx/H2OhNeAR4Bs73kcwegXuIT+tTEmxRjTzWsLxPexMGLm5kdSMNZaa4yJ2fOhjTGnAh8BD1hr9+Qcxo3lvllrs4B6xpjywCfA+f5GdOKMMVcD26y1KcaYJJ/DCYfm1tpNxpizgCnGmNU5X4zl72NhqLL4n01AlRzPK3ttsWCrV9bi/dzmtefVp2O1V86lPSKMMSVwiWKktfZjrzkQfTvEWrsLmAE0xQ1VHPoftpzxHO6D93o5YDvH3+dwawa0N8ZswA0RtQIGEPv9AsBau8n7uQ2X4BsTsO/jcfF70iRaHrgqaz1ugu3QZFqC33HlEWtV/jjB/W/+OOn2ird9FX+cdJvvtZ8O/IibcKvgbZ/uvXbkpNuVEeqTwY3bvnZEexD6diZQ3ts+GfgWuBoYxx8ngu/xtnvwx4ngD73tBP44EbweNwns+3cXSOJ/E9wx3y+gDFA2x/ZsoF0Qvo+F/p34HUA0PXBnNKzFjSc/7nc8ecQ4GtgMHMSNc96OG/edBqwDpub4MhpgkNefZUBijuN0xU0kpgK35WhPBJZ773kD7yr/CPSrOW6MeCmw2HtcGZC+1QUWeX1bDjzptVf3/mCk4v7AlvLaS3vPU73Xq+c41uNe/GvIcfaM399d/pgsYr5fXh+WeI8Vhz47CN/Hwj603IeIiORLcxYiIpIvJQsREcmXkoWIiORLyUJERPKlZCEiIvlSshARkXwpWYiISL6ULEQiwBjTyLvPQWljTBnvvha1/Y5LpKB0UZ5IhBhjnsNdxXwykGatfdHnkEQKTMlCJEK8O8YtADKAi61biVYkJmgYSiRy4oBTcXcCLO1zLCLHRZWFSIQYYz7HLeVdDahorb3X55BECkw3PxKJAGNMZ+CgtXaUMaY4MNsY08paO93v2EQKQpWFiIjkS3MWIiKSLyULERHJl5KFiIjkS8lCRETypWQhIiL5UrIQEZF8KVmIiEi+/h9EHU/GNH4ZdAAAAABJRU5ErkJggg==\n"
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": "(0.07629278861048339, 21.33424487500785)"
     },
     "execution_count": 28,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import numpy as np\n",
    "import scipy as sp   ##在numpy基础上实现的部分算法库\n",
    "\n",
    "import matplotlib.pyplot as plt  ##绘图库\n",
    "\n",
    "from scipy.optimize import leastsq  ##引入最小二乘法算法\n",
    "\n",
    "x = np.array([14.55, 296.35, 55950.05])\n",
    "y = np.array([1, 65.4965, 4289.8110])\n",
    "def linear_regression(x,y):\n",
    "    N = len(x)\n",
    "    sumx = sum(x)\n",
    "    sumy = sum(y)\n",
    "    sumx2 = sum(x**2)\n",
    "    sumxy = sum(x*y)\n",
    "    A = np.mat([[N,sumx],[sumx,sumx2]])\n",
    "    b = np.array([sumy,sumxy])\n",
    "\n",
    "    return np.linalg.solve(A,b)\n",
    "b,k = linear_regression(x,y)\n",
    "\n",
    "# reg = np.polyfit(x,y,deg=1) #输入训练样本  设置一元\n",
    "# ry = np.polyval(reg,x)\n",
    "ry = k*x+b\n",
    "plt.plot(x,y,'b.',label='y = hx+b')\n",
    "plt.plot(x,ry,'r',label='regression')\n",
    "plt.legend(loc=0) #左上角显示label\n",
    "plt.grid(True)\n",
    "plt.xlabel('x')\n",
    "plt.ylabel('y')\n",
    "# plt.loglog()\n",
    "plt.savefig('linear.png')\n",
    "plt.show()\n",
    "k, b"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%%\n"
    }
   }
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3.10.5 64-bit",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.5"
  },
  "orig_nbformat": 4,
  "vscode": {
   "interpreter": {
    "hash": "d4ad195ff334c471a543b0a7bb226f1a689063219ec9cc66cee2dec60707a1ec"
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}